﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace FloorDesigner
{
    public class LayoutManager
    {
        public event EventHandler<CopiedEventArgs<FrameworkElement>> CopiedElement;
        public event EventHandler<DroppedEventArgs> Dropped;
        public event EventHandler<DragStartedEventArgs> DragStarted;
        public event EventHandler<DraggingEventArgs> Dragging;
        public event EventHandler<ContainerEventArgs<FrameworkElement>> ContainerEnter;
        public event EventHandler<ContainerEventArgs<FrameworkElement>> ContainerExit;

        bool mouseCaptured;
        Panel dragStartPanel;
        int initialZIndex;
        bool clearIfNotDropped;
        Collection<Panel> previousDropContainer = new Collection<Panel>();

        Point dragInitPos;
        Thickness dragInitMargin;
        HorizontalAlignment dragInitHorizontalAlignment;
        VerticalAlignment dragInitVerticalAlignment;

        Transform dragInitRenderTransform;
        Transform dragInitTransformExcludeTranslate;
        Point dragInitRenderTransformOrigin;

        FrameworkElement TableElement { get; set; }

        public bool IsInDesignMode
        {
            get
            {
                return this.mouseCaptured;
            }
        }
        
        public IEnumerable<Panel> AllowedDropContainers { get; set; }
        
        public bool AllowCopy { get; set; }
        public bool OnceCopy { get; set; }

        public LayoutManager(FrameworkElement tableElement, IEnumerable<Panel> allowedDropContainers, bool _allowCopy, bool _copy)
        {
            this.TableElement = tableElement;
            this.AllowedDropContainers = allowedDropContainers;
            this.AllowCopy = _allowCopy;
            this.OnceCopy = _copy;

            this.BindEvents();
        }

        private void BindEvents()
        {
            this.TableElement.MouseLeftButtonUp += new MouseButtonEventHandler(TableElement_MouseLeftButtonUp);
            this.TableElement.KeyUp += new KeyEventHandler(TableElement_KeyUp);
            this.TableElement.MouseMove += new MouseEventHandler(TableElement_MouseMove_Drag);
        }

        public void StartDragging(object sender, MouseButtonEventArgs e)
        {
            if (!this.StartDragging(e)) throw new InvalidOperationException("Dragging not possible.");
        }

        public bool StartDragging(MouseButtonEventArgs e)
        {
            if (this.mouseCaptured) return false;

            InitDraggingMode(e);

            return true;
        }

        private void InitDraggingMode(MouseButtonEventArgs e)
        {
            DesignDraggedShape();

            SetDragDropElementLayoutToDefaultAndStore();

            CallIDropContainerEvent_OnDragStart();

            InitTableElementLayout(e);

            InitMouseForDragging();

            this.initialZIndex = SilverlightExtensions.GetZIndexAndSetMaxZIndex(this.TableElement);
            
            FocusTableElement();

            CallEvent_DragStarted();
        }

        private void DesignDraggedShape()
        {
            if (this.AllowCopy)
            {
                var objDraggedShape = this.TableElement.Copy();

                CallEvent_DesignTable(objDraggedShape);
                ((Panel)this.TableElement.Parent).Children.Add(objDraggedShape);

                if (this.OnceCopy)
                {
                    this.AllowCopy = false;
                    this.clearIfNotDropped = true;
                }
            }
        }

        private void SetDragDropElementLayoutToDefaultAndStore()
        {
            this.dragInitPos = new Point(Canvas.GetLeft(this.TableElement), Canvas.GetTop(this.TableElement));
            this.dragInitMargin = this.TableElement.Margin;
            this.dragInitHorizontalAlignment = this.TableElement.HorizontalAlignment;
            this.dragInitVerticalAlignment = this.TableElement.VerticalAlignment;
            this.dragInitRenderTransform = this.TableElement.RenderTransform;
            this.dragInitRenderTransformOrigin = this.TableElement.RenderTransformOrigin;

            Canvas.SetLeft(this.TableElement, 0.0);
            Canvas.SetTop(this.TableElement, 0.0);
            this.TableElement.Margin = new Thickness(0.0);
            this.TableElement.HorizontalAlignment = HorizontalAlignment.Left;
            this.TableElement.VerticalAlignment = VerticalAlignment.Top;
            this.TableElement.RenderTransformOrigin = new Point(0.5, 0.5);

            this.dragStartPanel = (Panel)this.TableElement.Parent;

            this.dragInitTransformExcludeTranslate = this.TableElement.RenderTransform.ExcludeTransform(typeof(TranslateTransform));
        }

        private void InitTableElementLayout(MouseButtonEventArgs e)
        {
            Panel highestPanel = SilverlightExtensions.GetFirstParentPanel(this.TableElement);

            MoveDragDropElementToPanel(highestPanel);

            var transform = this.CombineTransformations(e.GetPosition(highestPanel), null, 1.0, 1.0); 
            this.TableElement.RenderTransform = transform;
        }

        private void MoveDragDropElementToPanel(Panel highestPanel)
        {
            SilverlightExtensions.RemoveElementFromParentPanel(this.TableElement);
            if (highestPanel != null) highestPanel.Children.Add(this.TableElement);
        }

        private void InitMouseForDragging()
        {
            this.mouseCaptured = true;

            if (!this.TableElement.CaptureMouse()) return;

            this.TableElement.Cursor = Cursors.Hand;
        }

        private void FocusTableElement()
        {
            if (this.TableElement.GetType().GetAllBaseTypes().Contains(typeof(Control))) ((Control)this.TableElement).Focus();
        }

        void TableElement_MouseMove_Drag(object sender, MouseEventArgs e)
        {
            if (!this.mouseCaptured) return;

            DraggingModeStatus(e);
        }

        private void TableElement_KeyUp(object sender, KeyEventArgs e)
        {
            if (!this.mouseCaptured || e.PlatformKeyCode != 27) return;

            ClearDragDropOperation();
        }

        private void DraggingModeStatus(MouseEventArgs e)
        {
            if (this.GetType().BaseType == typeof(ContentControl))
                (((ContentControl)this.TableElement).Content as FrameworkElement).Opacity = 0.5;

            if (!this.TableElement.CaptureMouse())
            {
                ClearDragDropOperation();
                return;
            }

            KeepFocusWhileDragging();

            var dropTargets = SilverlightExtensions.GetSortedContainersByAbsoluteMousePos(
                e.GetPosition(null), this.AllowedDropContainers);
            var dropTarget = SilverlightExtensions.GetHighestContainerTarget(dropTargets);

            CallIDropContainer_OnDragMouseOver(dropTarget);

            CallEvent_Dragging(dropTarget);

            var translateTransform = TransformWhileDragging(e, dropTarget);
            this.TableElement.RenderTransform = translateTransform; 
            CallTargetLeaveAndEnterEvents(dropTargets);
        }

        private void KeepFocusWhileDragging()
        {
            if (FocusManager.GetFocusedElement() != this)
            {
                if (this.TableElement.GetType().GetAllBaseTypes().Contains(typeof(Control))) ((Control)this.TableElement).Focus();
            }
        }

        private void CallTargetLeaveAndEnterEvents(IEnumerable<Panel> dropTargets)
        {
            if (dropTargets == null) return;

            var enterTargets = from t in dropTargets
                               where !previousDropContainer.Contains(t)
                               select t;
            var leftTargets = from t in previousDropContainer
                              where !dropTargets.Contains(t)
                              select t;

            foreach (var t in enterTargets)
            {
                CallEvent_ContainerEnter(t);

                CallIDropContainer_OnElementContainerEnter(t);
            }
            foreach (var t in leftTargets)
            {
                CallEvent_ContainerExit(t);

                CallIDropContainer_OnElementContainerExit(t);
            }

            this.previousDropContainer = new Collection<Panel>();
            foreach (var p in dropTargets) this.previousDropContainer.Add(p);
        }

        private Transform TransformWhileDragging(MouseEventArgs e, Panel dropTarget)
        {
            Point newPositionRelativeToParent = e.GetPosition((UIElement)this.TableElement.Parent);

            double dropTargetScaleX = 1.0;
            double dropTargetScaleY = 1.0;
            ScaleTransform scaleTransform = null;

            CalculateScalingWhileDragging(dropTarget, ref dropTargetScaleX, ref dropTargetScaleY, ref scaleTransform);

            var translateTransform = this.CombineTransformations(newPositionRelativeToParent, scaleTransform, dropTargetScaleX, dropTargetScaleY);
            return translateTransform;
        }

        private void CalculateScalingWhileDragging(Panel dropTarget, ref double dropTargetScaleX, ref double dropTargetScaleY, ref ScaleTransform scaleTransform)
        {
            IZoomAbleContainer dropZoomContainer = null;
            var selfOrParentsImplementZoomInterface = SilverlightExtensions.SelfOrParentVisualsImplementInterface<IZoomAbleContainer>( dropTarget, typeof(IZoomAbleContainer), out dropZoomContainer);
            if (selfOrParentsImplementZoomInterface)
            {
                var diffZoomFactor = ZoomAbleContainerExtensions.GetDiffZoomFactorBy2Panels(this.CurrentContainerParent as IZoomAbleContainer, dropZoomContainer);

                scaleTransform = ZoomableScalingTransform(diffZoomFactor, diffZoomFactor);
                dropTargetScaleX = diffZoomFactor;
                dropTargetScaleY = diffZoomFactor;
            }
            else
            {
                if (dropTarget != null)
                {
                    var resVector = dropTarget.RenderTransform.GetTotalScales(new Vector2D(dropTargetScaleX, dropTargetScaleY));
                    scaleTransform = ZoomableScalingTransform(resVector.X, resVector.Y);
                    dropTargetScaleX = resVector.X;
                    dropTargetScaleY = resVector.Y;
                }
            }
        }

        private Panel CurrentContainerParent
        {
            get
            {
                foreach (var target in this.AllowedDropContainers)
                {
                    if (target != null && target.Children.Contains(this.TableElement)) return target;
                }

                return null;
            }
        }

        private ScaleTransform ZoomableScalingTransform(double diffZoomFactorX, double diffZoomFactorY)
        {
            var scaleTransform =
                new ScaleTransform()
                {
                    ScaleX = diffZoomFactorX,
                    ScaleY = diffZoomFactorY,
                    CenterX = this.TableElement.Width / 2.0d / diffZoomFactorX,
                    CenterY = this.TableElement.Height / 2.0d / diffZoomFactorY
                };
            return scaleTransform;
        }
       
        public Transform CombineTransformations(Point newPositionRelativeToTarget, ScaleTransform additionalScaleTransform, double diffZoomFactorX, double diffZoomFactorY)
        {
            var transformGroup = new TransformGroup();

            var translateTransform = new TranslateTransform()
            {
                X = newPositionRelativeToTarget.X - (this.TableElement.Width / 2.0d / diffZoomFactorX),
                Y = newPositionRelativeToTarget.Y - (this.TableElement.Height / 2.0d / diffZoomFactorY)
            };

            if (this.dragInitTransformExcludeTranslate != null) transformGroup.Children.Add(this.dragInitTransformExcludeTranslate);
            if (additionalScaleTransform != null) transformGroup.Children.Add(additionalScaleTransform);
            transformGroup.Children.Add(translateTransform);
            return transformGroup;
        }

        private void ClearDragDropOperation()
        {
            CancelDragOperation();

            CleanAfterDragDropEnded();

            CallEvent_Dropped();
        }

        private void TableElement_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            Drop(e);
        }

        private void Drop(MouseButtonEventArgs e)
        {
            if (!this.mouseCaptured) return;

            bool dropSuccessful = false;
            Panel dropTarget = null;
            try
            {
                var dropTargets = SilverlightExtensions.GetSortedContainersByAbsoluteMousePos(e.GetPosition(null), this.AllowedDropContainers);
                dropTarget = SilverlightExtensions.GetHighestContainerTarget(dropTargets);

                if (dropTarget != null)
                {
                    CallIDropContainer_OnDropping(dropTarget);

                    SilverlightExtensions.RemoveElementFromParentPanel(this.TableElement);

                    var dropTargetIsStack = dropTarget.GetType().InheritsType(typeof(StackPanel));
                    if (dropTargetIsStack) TableElement.RenderTransform = null;

                    dropTarget.Children.Add(this.TableElement);

                    if (!dropTargetIsStack)
                    {
                        var translation = TransformWhileDrop(e, dropTarget);
                        this.TableElement.RenderTransform = translation;
                    }

                    this.clearIfNotDropped = false;
                    
                    dropSuccessful = true;
                }
                else
                {
                    CancelDragOperation();

                    dropSuccessful = false;
                }
            }
            catch
            {
                CancelDragOperation();

                dropSuccessful = false;
            }
            finally
            {
                CleanAfterDragDropEnded();
                CallIDropContainer_OnDropped(dropTarget);
                CallEvent_Dropped(dropSuccessful, dropTarget);
            }
        }

        private Transform TransformWhileDrop(MouseButtonEventArgs e, Panel dropTarget)
        {
            var diffZoomFactor = ZoomAbleContainerExtensions.GetDiffZoomFactorBy2Panels(this.CurrentContainerParent, dropTarget);

            Point newPositionRelativeToTarget = e.GetPosition(dropTarget);

            var translation =
                this.CombineTransformations(
                    newPositionRelativeToTarget,
                    null,
                    diffZoomFactor,
                    diffZoomFactor);
            return translation;
        }

        private void CleanAfterDragDropEnded()
        {
            this.TableElement.RenderTransformOrigin = this.dragInitRenderTransformOrigin;

            this.TableElement.ReleaseMouseCapture();
            this.mouseCaptured = false;
            this.TableElement.Cursor = Cursors.Arrow;

            if (this.GetType().BaseType == typeof(ContentControl)) (((ContentControl)this.TableElement).Content as FrameworkElement).Opacity = 1.0; 
            this.TableElement.SetValue(Canvas.ZIndexProperty, this.initialZIndex);
        }

        private  void CancelDragOperation()
        {
            if (!this.clearIfNotDropped)
            {

                if (this.TableElement.Parent != null)
                {
                    ((Panel)this.TableElement.Parent).Children.Remove(this.TableElement);
                    this.dragStartPanel.Children.Add(this.TableElement);
                }

                Canvas.SetLeft(this.TableElement, this.dragInitPos.X);
                Canvas.SetTop(this.TableElement, this.dragInitPos.Y);
                this.TableElement.Margin = this.dragInitMargin;
                this.TableElement.HorizontalAlignment = this.dragInitHorizontalAlignment;
                this.TableElement.VerticalAlignment = this.dragInitVerticalAlignment;
                this.TableElement.RenderTransform = this.dragInitRenderTransform;
                this.TableElement.RenderTransformOrigin = this.dragInitRenderTransformOrigin;
            }
            else
            {
                SilverlightExtensions.RemoveElementFromParentPanel(this.TableElement);
            }
        }

        private void CallIDropContainer_OnElementContainerExit(Panel dropTarget)
        {
            IDropContainer dropContainer;
            if (SilverlightExtensions.SelfOrParentVisualsImplementInterface<IDropContainer>(
                    dropTarget, typeof(IDropContainer), out dropContainer))
            {
                dropContainer.OnElementContainerExit(this.TableElement);
            }
        }

        private void CallIDropContainer_OnElementContainerEnter(Panel dropTarget)
        {
            IDropContainer dropContainer;
            if (SilverlightExtensions.SelfOrParentVisualsImplementInterface<IDropContainer>(
                    dropTarget, typeof(IDropContainer), out dropContainer))
            {
                dropContainer.OnElementContainerEnter(this.TableElement);
            }
        }
        
        private void CallIDropContainer_OnDragMouseOver(Panel dropTarget)
        {
            IDropContainer dropContainer;
            if (SilverlightExtensions.SelfOrParentVisualsImplementInterface<IDropContainer>(
                    dropTarget, typeof(IDropContainer), out dropContainer))
            {
                dropContainer.OnDragMouseOver(this.TableElement);
            }
        }

        private void CallIDropContainerEvent_OnDragStart()
        {
            IDropContainer dropContainer;
            if (SilverlightExtensions.SelfOrParentVisualsImplementInterface<IDropContainer>(
                    this.dragStartPanel, typeof(IDropContainer), out dropContainer))
            {
                dropContainer.OnDragStart(this.TableElement);
            }
        }

        private void CallIDropContainer_OnDropping(Panel dropTarget)
        {
            IDropContainer dropContainer;
            if (SilverlightExtensions.SelfOrParentVisualsImplementInterface<IDropContainer>(
                    dropTarget, typeof(IDropContainer), out dropContainer))
            {
                dropContainer.OnDropping(this.TableElement);
            }
        }

        private void CallIDropContainer_OnDropped(Panel dropTarget)
        {
            IDropContainer dropContainer;
            if (SilverlightExtensions.SelfOrParentVisualsImplementInterface<IDropContainer>(
                    dropTarget, typeof(IDropContainer), out dropContainer))
            {
                ((IDropContainer)dropContainer).OnDropped(this.TableElement);
            }
        }

        private void CallEvent_DragStarted()
        {
            if (this.DragStarted != null) this.DragStarted(this, new DragStartedEventArgs(this.TableElement));
        }

        private void CallEvent_Dropped(bool dropSuccessful, Panel dropTarget)
        {
            if (this.Dropped != null) this.Dropped(this,  new DroppedEventArgs(this.TableElement, dropSuccessful, dropTarget));
        }

        private void CallEvent_Dragging(Panel dropTarget)
        {
            if (this.Dragging != null) this.Dragging(this, new DraggingEventArgs(this.TableElement, (dropTarget != null) ? true : false, dropTarget));
        }

        private void CallEvent_DesignTable(FrameworkElement objDraggedShape)
        {
            if (this.CopiedElement != null) this.CopiedElement(this, new CopiedEventArgs<FrameworkElement>(this.TableElement, objDraggedShape));
        }

        private void CallEvent_Dropped()
        {
            if (this.Dropped != null) this.Dropped( this, new DroppedEventArgs(this.TableElement, false, null));
        }

        private void CallEvent_ContainerEnter(Panel t)
        {
            if (this.ContainerEnter != null) this.ContainerEnter(this, new ContainerEventArgs<FrameworkElement>(this.TableElement, t));
        }

        private void CallEvent_ContainerExit(Panel t)
        {
            if (this.ContainerExit != null) this.ContainerExit(this, new ContainerEventArgs<FrameworkElement>(this.TableElement, t));
        }
    }
}
